package com.ojoin.trade.common.web.controller;

import static com.google.common.base.Strings.isNullOrEmpty;

import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import javax.validation.ValidationException;

import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.ldap.client.api.LdapConnectionConfig;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.google.common.base.Strings;
import com.ojoin.trade.common.exception.BusinessException;
import com.ojoin.trade.common.web.domain.LdapConfigRequest;
import com.ojoin.trade.common.web.domain.LdapConfigResponse;
import com.ojoin.trade.common.web.domain.LdapSettingsRequest;
import com.ojoin.trade.common.web.domain.LdapSettingsResponse;
import com.ojoin.trade.common.web.domain.Result;
import com.ojoin.trade.common.web.domain.Result.Status;
import com.ojoin.trade.common.web.ldap.LdapSettings;
import com.ojoin.trade.common.web.ldap.LdapSettingsService;
import com.ojoin.trade.common.web.ldap.support.LdapConnector;
import com.ojoin.trade.common.web.ldap.support.LdapEntry;
import com.ojoin.trade.common.web.ldap.support.TrustAllX509TrustManager;


@Controller
@RequestMapping(value = "/system/ldap")
public class LdapController  extends SimpleBaseController{
    private static final Logger LOG = LoggerFactory.getLogger(LdapController.class);

    @Autowired
    private LdapSettingsService ldapSettingsService;

    @Autowired
    private LdapSettings ldapSettings;

    @Autowired
    private LdapConnector ldapConnector;

    @RequestMapping(value ="settings", method = RequestMethod.GET,consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE,produces=MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public LdapSettingsResponse getLdapSettings() {
        final LdapSettings ldapSettings = ldapSettingsService.load();
        if (ldapSettings == null) {
            return LdapSettingsResponse.emptyDisabled();
        }

        return new LdapSettingsResponse(
                ldapSettings.isEnabled(),
                ldapSettings.getSystemUserName(),
                ldapSettings.getSystemPassword(),
                ldapSettings.getUri(),
                ldapSettings.isUseStartTls(),
                ldapSettings.isTrustAllCertificates(),
                ldapSettings.isActiveDirectory(),
                ldapSettings.getSearchBase(),
                ldapSettings.getSearchPattern(),
                ldapSettings.getDisplayNameAttribute(),
                ldapSettings.getDefaultGroup(),
                ldapSettings.getGroupMapping(),
                ldapSettings.getGroupSearchBase(),
                ldapSettings.getGroupIdAttribute(),
                ldapSettings.getAdditionalDefaultGroups(),
                ldapSettings.getGroupSearchPattern());
    }

    @RequestMapping(value ="test", method = RequestMethod.POST,consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE,produces=MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public LdapConfigResponse testLdapConfiguration(LdapConfigRequest request) {
        final LdapConnectionConfig config = new LdapConnectionConfig();
        final URI ldapUri = request.getLdapUri();
        config.setLdapHost(ldapUri.getHost());
        config.setLdapPort(ldapUri.getPort());
        config.setUseSsl(ldapUri.getScheme().startsWith("ldaps"));
        config.setUseTls(request.isUseStartTls());

        if (request.isTrustAllCertificates()) {
            config.setTrustManagers(new TrustAllX509TrustManager());
        }

        if (!isNullOrEmpty(request.getSystemUsername()) && !isNullOrEmpty(request.getSystemPassword())) {
            config.setName(request.getSystemUsername());
            config.setCredentials(request.getSystemPassword());
        }

        LdapNetworkConnection connection = null;
        try {
            try {
                connection = ldapConnector.connect(config);
            } catch (LdapException e) {
                return LdapConfigResponse.create(false, false, false, Collections.<String, String>emptyMap(), Collections.<String>emptySet(), e.getMessage());
            }

            if (null == connection) {
                return LdapConfigResponse.create(false, false, false, Collections.<String, String>emptyMap(), Collections.<String>emptySet(), "Could not connect to LDAP server");
            }

            boolean connected = connection.isConnected();
            boolean systemAuthenticated = connection.isAuthenticated();

            // the web interface allows testing the connection only, in that case we can bail out early.
            if (request.isTestConnectOnly()) {
                return LdapConfigResponse.create(connected, systemAuthenticated, false, Collections.<String, String>emptyMap(), Collections.<String>emptySet());
            }

            String userPrincipalName = null;
            boolean loginAuthenticated = false;
            Map<String, String> entryMap = Collections.emptyMap();
            String exception = null;
            Set<String> groups = Collections.emptySet();
            try {
                final LdapEntry entry = ldapConnector.search(
                        connection,
                        request.getSearchBase(),
                        request.getSearchPattern(),
                        "*",
                        request.getPrincipal(),
                        request.isActiveDirectory(),
                        request.getGroupSearchBase(),
                        request.getGroupIdAttribute(),
                        request.getGroupSearchPattern());
                if (entry != null) {
                    userPrincipalName = entry.getBindPrincipal();
                    entryMap = entry.getAttributes();
                    groups = entry.getGroups();
                }
            } catch (CursorException | LdapException e) {
                exception = e.getMessage();
            }

            try {
                loginAuthenticated = ldapConnector.authenticate(connection, userPrincipalName, request.getPassword());
            } catch (Exception e) {
                exception = e.getMessage();
            }

            return LdapConfigResponse.create(connected, systemAuthenticated, loginAuthenticated, entryMap, groups, exception);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (IOException e) {
                    LOG.warn("Unable to close LDAP connection.", e);
                }
            }
        }
    }

    @RequestMapping(value ="/settings", method = RequestMethod.PUT,consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE,produces=MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public void updateLdapSettings( LdapSettingsRequest request) throws ValidationException {

        ldapSettings.setSystemUsername(request.getSystemUsername());
        ldapSettings.setSystemPassword(request.getSystemPassword());
        ldapSettings.setUri(request.getLdapUri());
        ldapSettings.setUseStartTls(request.isUseStartTls());
        ldapSettings.setTrustAllCertificates(request.isTrustAllCertificates());
        ldapSettings.setActiveDirectory(request.isActiveDirectory());
        ldapSettings.setSearchPattern(request.getSearchPattern());
        ldapSettings.setSearchBase(request.getSearchBase());
        ldapSettings.setEnabled(request.isEnabled());
        ldapSettings.setDisplayNameAttribute(request.getDisplayNameAttribute());
        ldapSettings.setDefaultGroup(request.getDefaultGroup());
        ldapSettings.setGroupMapping(request.getGroupMapping());
        ldapSettings.setGroupSearchBase(request.getGroupSearchBase());
        ldapSettings.setGroupIdAttribute(request.getGroupIdAttribute());
        ldapSettings.setGroupSearchPattern(request.getGroupSearchPattern());
        ldapSettings.setAdditionalDefaultGroups(request.getAdditionalDefaultGroups());

        ldapSettingsService.save(ldapSettings);
    }

    @RequestMapping(value ="/settings", method = RequestMethod.DELETE,consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE,produces=MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public void deleteLdapSettings() {
        ldapSettingsService.delete();
    }

    @RequestMapping(value ="/settings/groups", method = RequestMethod.GET,consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE,produces=MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Map<String, String> readGroupMapping(){
        final LdapSettings ldapSettings = ldapSettingsService.load();
        return ldapSettings.getGroupMapping();
    }

    @RequestMapping(value ="/settings/groups", method = RequestMethod.PUT,consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE,produces=MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Result updateGroupMappingSettings(  Map<String, String> groupMapping) throws ValidationException {

        ldapSettings.setGroupMapping(groupMapping);
        ldapSettingsService.save(ldapSettings);

        return new Result(Status.OK,"");
    }

    @RequestMapping(value ="groups", method = RequestMethod.GET,consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE,produces=MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Set<String> readGroups(){
        final LdapSettings ldapSettings =   ldapSettingsService.load() ;

        if (!ldapSettings.isEnabled()) {
            throw new BusinessException("LDAP is disabled.");
        }
        if (Strings.isNullOrEmpty(ldapSettings.getGroupSearchBase()) || Strings.isNullOrEmpty(ldapSettings.getGroupIdAttribute())) {
            throw new BusinessException("LDAP group configuration settings are not set.");
        }

        final LdapConnectionConfig config = new LdapConnectionConfig();
        final URI ldapUri = ldapSettings.getUri();
        config.setLdapHost(ldapUri.getHost());
        config.setLdapPort(ldapUri.getPort());
        config.setUseSsl(ldapUri.getScheme().startsWith("ldaps"));
        config.setUseTls(ldapSettings.isUseStartTls());

        if (ldapSettings.isTrustAllCertificates()) {
            config.setTrustManagers(new TrustAllX509TrustManager());
        }

        if (!isNullOrEmpty(ldapSettings.getSystemUserName()) && !isNullOrEmpty(ldapSettings.getSystemPassword())) {
            config.setName(ldapSettings.getSystemUserName());
            config.setCredentials(ldapSettings.getSystemPassword());
        }

        try (LdapNetworkConnection connection = ldapConnector.connect(config)) {
            return ldapConnector.listGroups(connection,
                                            ldapSettings.getGroupSearchBase(),
                                            ldapSettings.getGroupSearchPattern(),
                                            ldapSettings.getGroupIdAttribute()
            );
        } catch (IOException | LdapException e) {
            LOG.error("Unable to retrieve available LDAP groups", e);
            throw new BusinessException("Unable to retrieve available LDAP groups", e);
        }
    }
}
